// c4.四个函数实现；char、int 和 * 指针类型；if、while、return 语句和表达式的声明。实现了自举，功能足够可以说明基本的编译原理了。功能齐全，满足编译器开发的，词法分析，语法分析，目标代码生成的三个基本步骤。
//中文版编译方式：zhi c4.c -o c4.exe
//使用方式：c4 hello.c 或 zhi -run c4.c hello.c  .-s,-d。-s是输出每行c语言及其对应的汇编语言。-d是输出汇编语言:c4 -s hello.c 或 c4 -d hello.c

//C4的源码中不能使用struct类型。为此，它选择使用数组来模拟struct结构。
//变量必须先声明再使用；变量声明不能带初始化表达式
//作用域只有全局和局部两种。局部变量必须全部在函数体的最开头声明，不能在中间声明，也不支持块作用域。所以符号表只要一个简单的线性符号表足矣，不需要为了支持嵌套作用域而实现嵌套符号表
//不支持数组的声明，不过还是支持通过指针做下标访问。
//不支持前向声明（forward declaration）。于是也就没办法做相互递归（mutal recursion）的函数调用了。这么一来就算想完全使用递归下降来实现语法分析也做不到，因为递归下降在分析表达式时必然会出现相互递归的情况，例如分析被括号包围的表达式。
//表达式时必然会出现相互递归的情况，例如分析被括号包围的表达式。

//函数调用前，先将参数逐个放入栈中
//加载到全局变量  3,
//变量入栈,
//加载到全局变量  4,
//变量入栈
//然后执行跳转到子程序跳转到子程序
//跳转到子程序 -145211380
//上述代码相当于调用函数
//xxxx(4,3)
//这时候栈的内容为：
//[3,4,下一行代码的pc位置]
//
//子程序执行完毕后，栈的内容为
//[3,4]
//重置堆栈位置，去掉参数
//栈跳转 2
//这时栈的内容为
//[]

//开辟新子程序  0
//开始执行函数，函数的局部变量为0个，所以为开辟新子程序 0，这是栈的内容为
//[3,4,下一行代码的pc位置,上一个程序的栈底]
//加载到局部变量  3
//加载整数
//变量入栈
//[3,4,下一行代码的pc位置,上一个程序的栈底,3]
//加载到局部变量  2
//加载整数
//ADD
//[3,4,下一行代码的pc位置,上一个程序的栈底,7]
//将栈顶存入的两个参数运算放入到寄存器中
//子程序退出
//离开函数，恢复上一个程序的pc位置，以及栈底
//[3,4]

//c4的中间代码是一个基于寄存器的vm。
//其只有一个寄存器变量，为"寄存器变量"
//有一个栈存储区，当前栈顶位置为sp，每个函数有自己的栈底bp，存储区域为栈区
//有一个代码区，当前执行代码位置为pc，存储区域为text area
//有一个数据区，存储区域为data area

#include <stdio.h>
#include <stdlib.h>
#include <memory.h>

// 防止编译器编译报错
#ifndef _WIN32
#include <unistd.h>  //标准符号常量和类型,
#endif

#include <fcntl.h>   //文件控制选项
#define int long long
char *源文件缓冲,
     *在源码中的当前位置,
     *数据的段;//数据段
int *stack,
    *代码的段,
    *当前标识符,
    *符号表,
    当前单词编码,
    当前单词值,
    当前单词数据类型,  // 当前表达式的类型
    局部变量偏移,// 局部变量在栈中的偏移
    当前行号,
    输出源码和汇编,
    打印执行的指令;

// C4所支持的输入源码的字符集限定在7-bit ASCII上，所以每个输入的字符只可能在[0, 127]的闭区间范围内，所以是128
//id[单词的存储类别] =

enum 单词标记符和类型
{
    单词类别_常量 = 128, 单词类别_函数, 单词类别_系统调用, 单词类别_全局变量, 单词类别_局部变量, 单词类别_标识符, //存储类型
    关键字_字符, 关键字_否则, 关键字_枚举, 关键字_如果, 关键字_整数, 关键字_返回, 关键字_取大小, 关键字_判断,    //关键字
    英_赋值号, 英_问号, 英_逻辑或, 英_逻辑与, 英_按位或, 英_按位异或, 英_按位与, 英_双等号, 英_不等于号, 英_小于号, 英_大于号, 英_小于等于, 英_大于等于, 英_按位左移, 英_按位右移, 英_加号, 英_减号, 英_星号, 英_除号, 英_百分号, 英_双加号, 英_双减号, 英_左方括号 //标点符号
};

//虚拟机指令集(操作码)。汇编操作码，汇编指令
enum 中间代码
{
    加载到局部变量,  //加载局部变量（加载到局部变量 2：将栈往上的2个位置变量加载到寄存器）。去局部变量地址,以[PC+1]地址,以bp为基址,地址载入累加器a
    加载到全局变量,  //加加载到全局变量局变量（将代码区位置指定的代码加载到寄存器） 将某个数放入寄存器 寄存器变量 中。加载到全局变量 [PC+1]作为为立即数载入累加器a
    跳转,  //跳转指令（跳转 2：将代码区跳转到2这个位置）。无条件跳转到[PC+1]
    跳转到子程序, //跳转到子程序（跳转到子程序 2：将下一个代码区位置保存到栈顶，并将代码区跳转到2这个位置）。将PC+2入栈作为返回地址,跳转到[PC+1]
    零跳转,  //零跳转（零跳转 2：如果全局变量为a，则跳转到下一个代码区域，否则跳转到2这个位置）。累加器为零时分支,当累加器a为0时跳转到[PC+1],不为零时跳转到PC+2继续执行
    非零跳转, //非零跳转(非零跳转 2:如果全局变量不为a，则跳转到下一个代码区域，否则跳转到2这个位置)。累加器不为零时分支,当累加器a不为0时跳转到[PC+1],为零时跳转到PC+2
    开辟新子程序, //开辟一个新的子程序(开辟新子程序 2:保存前一个程序的栈顶到当前栈上，并设置新栈顶，和局部变量区)。进入子程序,将bp压栈,基址bp指向栈顶,然后将栈顶生长[PC+1]字,作为参数传递空间
    栈跳转, //栈跳转(栈跳转 2：栈调整，往前2个位置)
    子程序退出, //子程序退出(栈回滚到当前程序的栈顶，然后恢复上一个程序的栈顶，再恢复上一个程序的代码区，最后恢复上一个程序的栈)。离开子程序,堆栈指针sp = bp,从堆栈中弹出基址bp,pc
    加载整数, //加载整数(将寄存器的整数变量加载出来).将对应地址中的整数载入 寄存器变量 中，要求 寄存器变量 中存放地址。以a为地址取int数
    加载字符, //加载字符(将寄存器的字符变量加载出来).将对应地址中的字符载入 寄存器变量 中，要求 寄存器变量 中存放地址。以a为地址取char
    保存整数, //保存整数(将栈顶指定位置的地址保存整数).将 寄存器变量 中的数据作为整数存放入地址中，要求栈顶存放地址。以栈顶为地址存int数并弹栈[[sp++]]=a
    保存字符, //保存字符(将栈顶指定位置的地址保存字符).将 寄存器变量 中的数据作为字符存放入地址中，要求栈顶存放地址。以栈顶为地址存char并弹栈[[sp++]]=a
    变量入栈,//保存栈(将寄存器变量保存到栈上)，将a压栈
    OR, XOR, AND, EQ, NE, LT, GT, LE, GE, SHL, SHR, ADD, SUB, MUL, DIV, MOD,//都是二元操作符，都是将栈顶变量取出来，然后与寄存器运算，再存回到寄存器
//	OPEN 调用C库函数open,堆栈传递2个参数(第一个参数先进栈,返回值存入累加器a,下同)
//	READ 调用C库函数read,堆栈传递2个参数
//	CLOS 调用C库函数close,堆栈传递2个参数
//	PRTF 调用C库函数printf,[pc+1]表明参数个数,传递至多六个参数
//	MALC 调用C库函数malloc,堆栈传递一个参数
//	MSET 调用C库函数memset,堆栈传递3个参数
//	MCMP 调用C库函数memcmp,堆栈传递3个参数
    OPEN, READ, CLOS, PRTF, MALC, FREE, MSET, MCMP, //都是系统调用，将栈顶变量作为参数，逐一打印出来。在符号表上的Class为Sys，Type为Int，Val为特殊值.C4在自举时，会把#include全忽略掉，然后在main()里hack一下，把依赖的外部函数先硬插进符号表里。
    EXIT //退出整个程序(将栈顶变量打印出来，作为返回值，并退出来)
};

// 支持的数据类型
enum 数据类型{ 数据类型_字符, 数据类型_整数, 数据类型_指针 };
// 标识符偏移（因为我们不能创建一个 ident 结构）,符号表条目的字段索引，`符号表元素个数` 除外。
// `单词的值`：关联值。
// `备份_单词的类别`：`单词的存储类别` 字段的备份字段。
// `备份_单词的数据类型`：`单词值的数据类型` 字段的备份字段。
// `备份_变量的值`：`单词的值` 字段的备份字段。
// `符号表元素个数`: 符号表条目大小。
//因为作者没有实现结构体,所以[id]指向的空间被分割为Idsz大小的块(模拟结构体)
//当id指向块首时,id[0] == id[Tk] 访问的就是Tk成员的数据(一般是指针)
//其实这种做法，在高级语言里可以理解为：没有对象的多元素数组，来模拟对象
//以下是符号表的各条目：
//单词的字符串地址 指向的是这个identifier的Name
//单词值的数据类型 为数据类型(比如返回值类型),如CHAR,数据类型_整数,数据类型_整数+数据类型_指针
//单词的存储类别 为类型,如Num(常量数值),单词类别_函数(函数),单词类别_系统调用(系统调用),Glo全局变量,单词类别_局部变量 局部变量
//符号表是一个长数组，每个数组的元素为符号描述，元素长度为Idsz
//每个符号元素包括的信息为：
//单词的编码 符号类型，id类型，用来区分是否为关键字的
//单词的存储类别 变量类型，单词类别_常量 = 128（常数）单词类别_函数（函数）单词类别_系统调用（系统调用） 单词类别_全局变量（全局变量） 单词类别_局部变量（本地变量） 单词类别_标识符
//单词值的数据类型 变量数值类型，char,int,ptr,还是*char,*int,*ptr,还是**char,**int,**ptr
//单词的值 单词的值，对应不同的数据类型有不同的值

enum 符号表存放{ 单词的编码, 单词的哈希值, 单词的字符串地址, 单词的存储类别, 单词值的数据类型, 单词的值, 备份_单词的类别, 备份_单词的数据类型, 备份_变量的值, 符号表元素个数 };

// 第一步：词法分析器（读出下一个的token标识符,自动忽略空白字符）。每个标记格式 (当前单词编码, 当前单词值),如100，分析结果是 （number,100）
//词法分析，在next()函数中实现。
//每解析一个词语，就将
//结果类型写入到tk变量，
//变量值写入到ival变量
//id类的变量还会顺道写入到符号表中
void 取下个单词()
{              // 标识符=输入的符号
    char *pp;  // 标识符的起始位置
    //用户源代码逐个读取字符：主函数中已经把源代码读入了源代码缓冲区，以`\0`结尾。现在获取当前字符。当前字符不是`\0`，识别到`\0` 结束循环。
    //用循环来忽略空白字符,不过不能被词法分析器识别的字符都被认为是空白字符 比如 '@', '$'
    while (当前单词编码 = *源文件缓冲)
    {
        //    printf("当前单词编码:[%c]\n",当前单词编码);
        // 指向下一个字符。
        ++源文件缓冲;
        // 1、打印输出信息
        if (当前单词编码 == '\n')
        {
            if (输出源码和汇编)//命令行指明-s参数,输出源代码和对应字节码
            {
                // 打印源代码行
                printf("%ld: s%.*s", 当前行号, 源文件缓冲 - 在源码中的当前位置, 在源码中的当前位置);
                // 将 `在源码中的当前位置` 指向最后一个换行符。
                在源码中的当前位置 = 源文件缓冲;
                // 虽然有打印指令。
                while (代码的段 < stack)
                {
                    // 打印操作码。
                    printf("%8.4s", &"加载到局部变量 ,加载到全局变量 ,跳转 ,跳转到子程序 ,零跳转  ,非零跳转 ,开辟新子程序 ,栈跳转 ,子程序退出 ,加载整数  ,加载字符  ,保存整数  ,保存字符  ,变量入栈 ,"
                                     "OR  ,XOR ,AND ,EQ  ,NE  ,LT  ,GT  ,LE  ,GE  ,SHL ,SHR ,ADD ,SUB ,MUL ,DIV ,MOD ,"
                                     "OPEN,READ,CLOS,PRTF,MALC,FREE,MSET,MCMP,EXIT,"[*++代码的段 * 5]);
                    // 如果操作码 <= 栈跳转，它有操作数。
                    // 打印操作数。
                    if (*代码的段 <= 栈跳转) printf(" %d\n", *++代码的段); else printf("\n");//栈跳转之前的指令均有操作数
                }
            }
            ++当前行号;
        }else if (当前单词编码 == '#') //2、遇到宏处理操作,#由于不支持宏处理,所以直接跳过
        {
            while (*源文件缓冲 != 0 && *源文件缓冲 != '\n')
            {
                ++源文件缓冲;// 判断 #开头的头文件.忽略当前行的操作
            }
        }else if ((当前单词编码 >= 'a' && 当前单词编码 <= 'z') || (当前单词编码 >= 'A' && 当前单词编码 <= 'Z') || 当前单词编码 == '_')
        {
            //3、解析合法的变量名,判断字符串
            pp = 源文件缓冲 - 1;//因为有++源文件缓冲,pp回退一个字符,pp指向 [这个符号] 的首字母
            // 当前字符是字母、数字或下划线。
            while ((*源文件缓冲 >= 'a' && *源文件缓冲 <= 'z') || (*源文件缓冲 >= 'A' && *源文件缓冲 <= 'Z') || (*源文件缓冲 >= '0' && *源文件缓冲 <= '9') || *源文件缓冲 == '_')
            {
                当前单词编码 = 当前单词编码 * 147 + *源文件缓冲++;  // 使用当前字符计算哈希值。
            }
            当前单词编码 = (当前单词编码 << 6) + (源文件缓冲 - pp);// 将哈希值与字符串长度结合起来。
            当前标识符 = 符号表;                           // 将 `当前标识符` 指向符号表。
            // 当前符号表条目正在使用中。
            while (当前标识符[单词的编码])
            {
                // 如果当前符号表条目的哈希值相等且名称相等，则表示该名称之前已经出现过。将令牌类型设置为条目的令牌类型。
                if (当前单词编码 == 当前标识符[单词的哈希值] && !memcmp((char*)当前标识符[单词的字符串地址], pp, 源文件缓冲 - pp))
                { //找到同名,则tk = id[Tk] (将id看做结构体,访问其Tk成员,解释见上)
                    当前单词编码 = 当前标识符[单词的编码]; return; //代表该标识符已经被识别
                }
                // 指向下一个表条目。继续循环identifier表.
                当前标识符 = 当前标识符 + 符号表元素个数;
            }
            //找不到，保存新的标识符到符号表中
            当前标识符[单词的字符串地址] = (int)pp;
            当前标识符[单词的哈希值] = 当前单词编码;// 存储名称的哈希值。
            当前单词编码 = 当前标识符[单词的编码] = 单词类别_标识符;//设置单词类型为identifier
            return;
        }
        else if (当前单词编码 >= '0' && 当前单词编码 <= '9') {// 4、解析八进制、十进制、十六进制数字.第一位为数字,认为是数值
            if (当前单词值 = 当前单词编码 - '0') { while (*源文件缓冲 >= '0' && *源文件缓冲 <= '9') 当前单词值 = 当前单词值 * 10 + *源文件缓冲++ - '0';//第一位不为0,认为是十进制数
            }else if (*源文件缓冲 == 'x' || *源文件缓冲 == 'X') {//第一位为0,且以x开头,认为是16进制数
                // 将十六进制表示法转换为值。
                while ((当前单词编码 = *++源文件缓冲) && ((当前单词编码 >= '0' && 当前单词编码 <= '9') || (当前单词编码 >= 'a' && 当前单词编码 <= 'f') || (当前单词编码 >= 'A' && 当前单词编码 <= 'F')))
                    当前单词值 = 当前单词值 * 16 + (当前单词编码 & 15) + (当前单词编码 >= 'A' ? 9 : 0);
            }else {
                //八进制：如果当前字符是 `0` 并且后面的字符不是 `x` 或`X`，它是八进制表示法。将八进制表示法转换为值。
                while (*源文件缓冲 >= '0' && *源文件缓冲 <= '7') 当前单词值 = 当前单词值 * 8 + *源文件缓冲++ - '0';
            }
            当前单词编码 = 单词类别_常量;//token 为数值型,返回
            return;
        }
        else if (当前单词编码 == '/')
        {// 5、解析注释，只支持单行注释
            // 如果当前字符为`/`，则为注释或除法运算符。
            // 如果后面的字符是`/`，则为注释。
            if (*源文件缓冲 == '/')
            {
                // 指向下一个字符。
                ++源文件缓冲;
                // 当前字符不是 `\0` 而当前字符不是新队。跳过当前字符。
                while (*源文件缓冲 != 0 && *源文件缓冲 != '\n')
                {
                    ++源文件缓冲;
                }
            }
            else
            {// 如果后面的字符不是`/`，则为除法运算符。
                // 设置令牌类型。
                当前单词编码 = 英_除号;
                return;
            }
        }
        else if (当前单词编码 == '\'' || 当前单词编码 == '"')
        {//引号开头,认为是字符(串).// 6、解析字符串
            //判断双引号和单引号，存储数据缓冲区的当前位置。
            pp = 数据的段;
            // 当前字符不是`\0`并且当前字符不是引号字符。
            while (*源文件缓冲 != 0 && *源文件缓冲 != 当前单词编码)
            {//直到找到匹配的引号为止
                if ((当前单词值 = *源文件缓冲++) == '\\')
                {// 解析转义字符
                    // 如果后面的字符是`n`，则是换行符，
                    if ((当前单词值 = *源文件缓冲++) == 'n') 当前单词值 = '\n';// '\n' 认为是'\n' 其他直接忽略'\'转义
                }
                // 如果是字符串常量，则将当前字符复制到数据缓冲区。
                if (当前单词编码 == '"')
                {
                    *数据的段++ = 当前单词值;//如果是双引号,认为是字符串,向data拷贝字符
                }
            }
            // 指向下一个字符。
            ++源文件缓冲;
            // 如果是字符串常量，则使用字符串的地址作为令牌的地址关联值。令牌类型为`"`。如果是字符常量，则使用字符的值作为tokens标记的值关联值。将令牌类型设置为数字常量。
            if (当前单词编码 == '"')
            {
                当前单词值 = (int)pp; //双引号则ival指向data中字符串开始,单引号则认为是数字
            }else
            {
                当前单词编码 = 单词类别_常量;
            }
            return;
        }
            // 7、解析运算符号，和指令一一对应
        else if (当前单词编码 == '=') { if (*源文件缓冲 == '=') { ++源文件缓冲; 当前单词编码 = 英_双等号; } else 当前单词编码 = 英_赋值号; return; }//等于,赋值
        else if (当前单词编码 == '+') { if (*源文件缓冲 == '+') { ++源文件缓冲; 当前单词编码 = 英_双加号; } else 当前单词编码 = 英_加号; return; }//加,自增
        else if (当前单词编码 == '-') { if (*源文件缓冲 == '-') { ++源文件缓冲; 当前单词编码 = 英_双减号; } else 当前单词编码 = 英_减号; return; }//减,自减
        else if (当前单词编码 == '!') { if (*源文件缓冲 == '=') { ++源文件缓冲; 当前单词编码 = 英_不等于号; } return; }//不等于
        else if (当前单词编码 == '<') { if (*源文件缓冲 == '=') { ++源文件缓冲; 当前单词编码 = 英_小于等于; } else if (*源文件缓冲 == '<') { ++源文件缓冲; 当前单词编码 = 英_按位左移; } else 当前单词编码 = 英_小于号; return; }// <,<=, <<
        else if (当前单词编码 == '>') { if (*源文件缓冲 == '=') { ++源文件缓冲; 当前单词编码 = 英_大于等于; } else if (*源文件缓冲 == '>') { ++源文件缓冲; 当前单词编码 = 英_按位右移; } else 当前单词编码 = 英_大于号; return; }//>,>= >>
        else if (当前单词编码 == '|') { if (*源文件缓冲 == '|') { ++源文件缓冲; 当前单词编码 = 英_逻辑或; } else 当前单词编码 = 英_按位或; return; }//逻辑或,或
        else if (当前单词编码 == '&') { if (*源文件缓冲 == '&') { ++源文件缓冲; 当前单词编码 = 英_逻辑与; } else 当前单词编码 = 英_按位与; return; }
        else if (当前单词编码 == '^') { 当前单词编码 = 英_按位异或; return; }
        else if (当前单词编码 == '%') { 当前单词编码 = 英_百分号; return; }
        else if (当前单词编码 == '*') { 当前单词编码 = 英_星号; return; }
        else if (当前单词编码 == '[') { 当前单词编码 = 英_左方括号; return; }
        else if (当前单词编码 == '?') { 当前单词编码 = 英_问号; return; }
        else if (当前单词编码 == '~' || 当前单词编码 == ';' || 当前单词编码 == '{' || 当前单词编码 == '}' || 当前单词编码 == '(' || 当前单词编码 == ')' || 当前单词编码 == ']' || 当前单词编码 == ',' || 当前单词编码 == ':') return;//不做处理
    }

}

// `level`：表示运算符,因为各个运算符token是按照优先级生序排列的,所以lev大表示优先级高.根据不同的预算符，将变量计算，并将结果均写入到寄存器中。
//解析表达式()负责运算符语句的语法分析，包括加减乘除赋值等等
void 解析表达式(int level)
{
    int 参数计数, *d;  // 参数统计
    if (!当前单词编码) { printf("%d: 表达式中出现意外的文件结尾\n", 当前行号); exit(-1); }// 如果当前单词不存在，则打印错误并退出程序。
    else if (当前单词编码 == 单词类别_常量) { *++stack = 加载到全局变量; *++stack = 当前单词值; 取下个单词(); 当前单词数据类型 = 数据类型_整数; }//2、解析数字表达式.直接取立即数作为表达式值，用 加载到全局变量 指令将它加载到 AX 中
        //3、解析字符串常量
    else if (当前单词编码 == '"') {
        *++stack = 加载到全局变量;
        *++stack = 当前单词值;
        取下个单词();
        while (当前单词编码 == '"') 取下个单词();// 当前tokens标记为字符串常量时，它是相邻字符串常量，例如“ABC”“def”。在 `取下个单词` 中，字符串的字符已被复制到数据缓冲区。这实现了相邻字符串常量的连接。//连续的'"' 处理C风格多行文本 比如["abc" "def"]
        数据的段 = (char*)((int)数据的段 + sizeof(int) & -sizeof(int)); //字节对齐到int，在末尾追加结束符。// 将 `数据的段` 指向下一个 int 对齐的地址。例如`-sizeof(int)` 是 -4，即 0b11111100。这保证在字符串后至少留下一个 '\0'。设置结果值类型为源代码字符串指针。数据类型_字符 + 数据类型_指针 = 数据类型_指针 因为 数据类型_字符 是 0。
        当前单词数据类型 = 数据类型_指针;
    }
        //4、解析返回类型表达式
    else if (当前单词编码 == 关键字_取大小) {
        取下个单词(); if (当前单词编码 == '(') 取下个单词(); else { printf("%d: 应为取大小的左圆括号\n", 当前行号); exit(-1); }
        当前单词数据类型 = 数据类型_整数; if (当前单词编码 == 关键字_整数) 取下个单词(); else if (当前单词编码 == 关键字_字符) { 取下个单词(); 当前单词数据类型 = 数据类型_字符; }
        while (当前单词编码 == 英_星号) { 取下个单词(); 当前单词数据类型 = 当前单词数据类型 + 数据类型_指针; }//当前令牌是`*`，它是指针类型。多级指针,每多一级加PTR
        if (当前单词编码 == ')') 取下个单词(); else { printf("%d: 应为取大小的右圆括号\n", 当前行号); exit(-1); }
        *++stack = 加载到全局变量; // 添加`加载到全局变量`指令以加载操作数值的大小到寄存器。
        *++stack = (当前单词数据类型 == 数据类型_字符) ? sizeof(char) : sizeof(int);//除了char是一字节,int和多级指针都是int大小
        当前单词数据类型 = 数据类型_整数;
    }
        // 5、解析变量取值与函数调用
    else if (当前单词编码 == 单词类别_标识符) {
        d = 当前标识符; // 存储标识符的符号表入口地址。
        取下个单词();
        if (当前单词编码 == '(') {// 如果当前单词为`(`，则为函数调用。
            取下个单词();
            参数计数 = 0;  //形参个数
            while (当前单词编码 != ')') { 解析表达式(英_赋值号); *++stack = 变量入栈; ++参数计数; if (当前单词编码 == ',') 取下个单词(); }//计算实参的值，压栈(传参)。当前令牌不是`)`。解析参数表达式。添加`变量入栈`指令将参数压入堆栈。增加参数计数。如果当前tokens标记为`,`，则跳过。
            // Skip `)`
            取下个单词();
            if (d[单词的存储类别] == 单词类别_系统调用) *++stack = d[单词的值];//系统调用,如malloc,memset,d[val]为opcode。将系统调用的操作码添加到指令缓冲区。
            else if (d[单词的存储类别] == 单词类别_函数) { *++stack = 跳转到子程序; *++stack = d[单词的值]; }//用户定义函数,d[单词的值]为函数入口地址。将 `跳转到子程序` 操作码和函数地址添加到指令缓冲区。
            else { printf("%d: 错误的函数调用\n", 当前行号); exit(-1); }
            if (参数计数) { *++stack = 栈跳转; *++stack = 参数计数; }//因为用栈传参,调整栈。如果有参数。将 `栈跳转` 指令和参数计数添加到指令缓冲区以从函数调用返回后从堆栈中弹出参数。
            当前单词数据类型 = d[单词值的数据类型];// 设置结果值类型为系统调用或函数的返回类型
        }
        else if (d[单词的存储类别] == 单词类别_常量) { *++stack = 加载到全局变量; *++stack = d[单词的值]; 当前单词数据类型 = 数据类型_整数; }//d[单词的存储类别] == 单词类别_常量,处理枚举(只有枚举是Class==单词类别_常量).添加`加载到全局变量` 指令将枚举值加载到寄存器中。设置结果值类型为`数据类型_整数`。
        else {// 如果以上都不是，则假设它是一个变量名。
            // 变量先取地址然后再加载字符/加载整数。6S71X如果是局部变量，添加 `加载到局部变量` 操作码和局部变量的偏移到指令缓冲区以加载局部变量的地址登记
            if (d[单词的存储类别] == 单词类别_局部变量) { *++stack = 加载到局部变量; *++stack = 局部变量偏移 - d[单词的值]; }//取地址,d[单词的值]是局部变量偏移量
            else if (d[单词的存储类别] == 单词类别_全局变量) { *++stack = 加载到全局变量; *++stack = d[单词的值]; }//取地址,d[单词的值] 是全局变量指针.添加`加载到全局变量`指令加载全局变量要注册的变量地址。
            else { printf("%d: 未定义的变量\n", 当前行号); exit(-1); }
            *++stack = ((当前单词数据类型 = d[单词值的数据类型]) == 数据类型_字符) ? 加载字符 : 加载整数;// 2WQE9添加`加载字符`/`加载整数`指令加载寄存器地址上的值注册。
        }
    }
        // 6、解析强制转换，获取转换的类型，并直接修改类型的值
    else if (当前单词编码 == '(') {
        取下个单词();
        if (当前单词编码 == 关键字_整数 || 当前单词编码 == 关键字_字符) {// 如果当前tokens标记是 `int` 或 `char`，则将其强制转换。
            参数计数 = (当前单词编码 == 关键字_整数) ? 数据类型_整数 : 数据类型_字符; 取下个单词();// 获取演员的基本数据类型。
            while (当前单词编码 == 英_星号) { 取下个单词(); 参数计数 = 参数计数 + 数据类型_指针; }// 当前令牌是`*`，它是指针类型。将 `数据类型_指针` 添加到转换的数据类型。
            if (当前单词编码 == ')') 取下个单词(); else { printf("%d: bad cast\n", 当前行号); exit(-1); }// 如果当前tokens标记不是`)`，则打印错误并退出程序。
            解析表达式(英_双加号);//高优先级。解析转换表达式。使用 `英_双加号` 以在表达式中只允许 `++`、`--`、`[]` 运算符。
            // 将结果值类型设置为强制转换的数据类型。
            当前单词数据类型 = 参数计数;
        }
            // 如果当前tokens标记不是 `int` 或 `char`，则是表达式 in括弧。
        else {
            // 解析表达式。一般语法括号
            解析表达式(英_赋值号);
            // 如果当前tokens标记不是`)`，则打印错误并退出程序。
            if (当前单词编码 == ')') 取下个单词(); else { printf("%d: 应为闭合右圆括号\n", 当前行号); exit(-1); }
        }
    }
        // 7、解析指针取值
    else if (当前单词编码 == 英_星号) {//取指针指向值
        取下个单词();
        解析表达式(英_双加号);//高优先级。解析操作数表达式。使用 `英_双加号` 以在表达式中只允许 `++`、`--`、`[]` 运算符。
        // 如果操作数值类型不是指针，则打印错误并退出程序。
        if (当前单词数据类型 > 数据类型_整数) 当前单词数据类型 = 当前单词数据类型 - 数据类型_指针; else { printf("%d: 错误的取消引用\n", 当前行号); exit(-1); }
        // 添加`加载字符`/`加载整数`指令加载寄存器地址上的值注册。
        *++stack = (当前单词数据类型 == 数据类型_字符) ? 加载字符 : 加载整数;
    }
        // 8、解析取址操作。如果当前tokens标记为`&`，则为取地址运算符。
    else if (当前单词编码 == 英_按位与) {
        // 读取令牌。解析操作数表达式。使用 `英_双加号` 以在表达式中只允许 `++`、`--`、`[]` 运算符。
        取下个单词(); 解析表达式(英_双加号);
        // address-of 运算符的操作数应该是一个变量。6S71X 增加了获取变量地址的指令。只需要去掉2WQE9添加的`加载字符`/`加载整数`指令即可。如果当前指令是`加载字符`/`加载整数`，删除它，否则打??印错误并退出程序。
        if (*stack == 加载字符 || *stack == 加载整数) --stack; //根据上面的代码,token为变量时都是先取地址再加载整数/加载字符,所以--e就变成了取地址到a
        else { printf("%d: bad address-of\n", 当前行号); exit(-1); }
        // 将结果值类型设置为指向当前值类型的指针。
        当前单词数据类型 = 当前单词数据类型 + 数据类型_指针;
    }
        // 9、解析运算符号表达式
        // 如果当前tokens标记为`!`，则为布尔否定运算符。添加计算 `x == 0` 的指令，因为 `!x` 等价于`x == 0`。设置结果值类型为`数据类型_整数`。
    else if (当前单词编码 == '!') { 取下个单词(); 解析表达式(英_双加号); *++stack = 变量入栈; *++stack = 加载到全局变量; *++stack = 0; *++stack = EQ; 当前单词数据类型 = 数据类型_整数; }//!x相当于 x==0
        // 如果当前tokens标记为`~`，则为按位求逆运算符。 添加指令来计算 `x ^ -1` 因为 `~x` 等价于`x ^ -1`。 设置结果值类型为`数据类型_整数`。
    else if (当前单词编码 == '~') { 取下个单词(); 解析表达式(英_双加号); *++stack = 变量入栈; *++stack = 加载到全局变量; *++stack = -1; *++stack = XOR; 当前单词数据类型 = 数据类型_整数; }//~x 相当于x ^ -1
        // 如果当前tokens标记为`+`，则为一元加法运算符。 读取令牌。解析操作数表达式。设置结果值类型为`数据类型_整数`。
    else if (当前单词编码 == 英_加号) { 取下个单词(); 解析表达式(英_双加号); 当前单词数据类型 = 数据类型_整数; }
        // 如果当前tokens标记为`-`，则为一元减法运算符。
    else if (当前单词编码 == 英_减号) {
        // 读取令牌。添加`加载到全局变量`指令来加载数字常量的否定值或`-1`注册。
        取下个单词(); *++stack = 加载到全局变量;
        // 如果操作数是数字常量，则向指令缓冲区添加负值。如果操作数不是数字常量，则将 `-1` 添加到指令缓冲区。添加`变量入栈` 指令将寄存器中的 `-1` 压入堆栈。解析操作数表达。添加 `MUL` 指令将堆栈上的 `-1` 乘以寄存器中的操作数值。
        if (当前单词编码 == 单词类别_常量) { *++stack = -当前单词值; 取下个单词(); }//数值,取负
        else { *++stack = -1; *++stack = 变量入栈; 解析表达式(英_双加号); *++stack = MUL; }//乘-1
        // 设置结果值类型为`数据类型_整数`。
        当前单词数据类型 = 数据类型_整数;
    }

        // // 10、解析自增、自减。如果当前tokens标记是前缀递增或递减运算符。
    else if (当前单词编码 == 英_双加号 || 当前单词编码 == 英_双减号) {//处理++x,--x//x--,x++在后面处理
        // 存储当前令牌类型。读取令牌。解析操作数表达式。
        参数计数 = 当前单词编码; 取下个单词(); 解析表达式(英_双加号);
        // 如果当前指令是 `加载字符`，则在 `加载字符` 之前插入一条 `变量入栈` 指令将寄存器中的变量地址压入堆栈以供 `保存字符` 使用下面添加了指令。
        if (*stack == 加载字符) { *stack = 变量入栈; *++stack = 加载字符; }//地址压栈(下面保存字符/保存整数用到),再取数
            // 如果当前指令是`加载整数`，则在`加载整数`之前插入一条`变量入栈`指令将寄存器中的变量地址压入堆栈供`保存整数`使用下面添加了指令。
        else if (*stack == 加载整数) { *stack = 变量入栈; *++stack = 加载整数; }
            // 否则打印错误并退出程序。
        else { printf("%d: 增量前的左值错误\n", 当前行号); exit(-1); }
        // 添加`变量入栈`指令将寄存器中的操作数值压入堆栈供下面添加的 `ADD`/`SUB` 指令使用。
        *++stack = 变量入栈;//将数值压栈
        // 添加`加载到全局变量`指令来加载增量/减量值到寄存器。
        *++stack = 加载到全局变量; *++stack = (当前单词数据类型 > 数据类型_指针) ? sizeof(int) : sizeof(char);//指针则加减一字,否则加减1
        // 添加`ADD`/`SUB`指令来计算结果值。
        *++stack = (参数计数 == 英_双加号) ? ADD : SUB;//运算
        // 添加`保存字符`/`保存整数`指令将寄存器中的结果值保存到地址保存在堆栈中。
        *++stack = (当前单词数据类型 == 数据类型_字符) ? 保存字符 : 保存整数;//存回变量
    }
    else
    {
        printf("%d: 不正确的表达\n", 当前行号); exit(-1);
    }
    //爬山法
    //tk为ASCII码的都不会超过Num=128
    // “优先爬升”或“自上而下运算符优先”方法
    while (当前单词编码 >= level) {
        参数计数 = 当前单词数据类型;//“当前单词数据类型”在递归过程中可能会改变,所以备份当前处理的表达式类型
        // 11、解析赋值运算
        if (当前单词编码 == 英_赋值号) {
            取下个单词();
            if (*stack == 加载字符 || *stack == 加载整数) *stack = 变量入栈; //左边被tk=Id中变量部分处理过了,将地址压栈
            else { printf("%d: 赋值中的左值错误\n", 当前行号); exit(-1); }
            解析表达式(英_赋值号); *++stack = ((当前单词数据类型 = 参数计数) == 数据类型_字符) ? 保存字符 : 保存整数;//取得右值expr的值,作为a=expr的结果
        }
            //12、解析三目运算符
        else if (当前单词编码 == 英_问号) {//x?a:b和if类似,除了不能没有else
            取下个单词();
            *++stack = 零跳转; d = ++stack;
            解析表达式(英_赋值号);
            if (当前单词编码 == ':') 取下个单词(); else { printf("%d: 条件缺失冒号\n", 当前行号); exit(-1); }
            *d = (int)(stack + 3); *++stack = 跳转; d = ++stack;
            解析表达式(英_问号);
            *d = (int)(stack + 1);
        }
        else if (当前单词编码 == 英_逻辑或) { 取下个单词(); *++stack = 非零跳转; d = ++stack; 解析表达式(英_逻辑与); *d = (int)(stack + 1); 当前单词数据类型 = 数据类型_整数; }
        else if (当前单词编码 == 英_逻辑与) { 取下个单词(); *++stack = 零跳转;  d = ++stack; 解析表达式(英_按位或);  *d = (int)(stack + 1); 当前单词数据类型 = 数据类型_整数; }
        else if (当前单词编码 == 英_按位或) { 取下个单词(); *++stack = 变量入栈; 解析表达式(英_按位异或); *++stack = OR;  当前单词数据类型 = 数据类型_整数; }
        else if (当前单词编码 == 英_按位异或) { 取下个单词(); *++stack = 变量入栈; 解析表达式(英_按位与); *++stack = XOR; 当前单词数据类型 = 数据类型_整数; }
        else if (当前单词编码 == 英_按位与) { 取下个单词(); *++stack = 变量入栈; 解析表达式(英_双等号);  *++stack = AND; 当前单词数据类型 = 数据类型_整数; }
        else if (当前单词编码 == 英_双等号) { 取下个单词(); *++stack = 变量入栈; 解析表达式(英_小于号);  *++stack = EQ;  当前单词数据类型 = 数据类型_整数; }
        else if (当前单词编码 == 英_不等于号) { 取下个单词(); *++stack = 变量入栈; 解析表达式(英_小于号);  *++stack = NE;  当前单词数据类型 = 数据类型_整数; }
        else if (当前单词编码 == 英_小于号) { 取下个单词(); *++stack = 变量入栈; 解析表达式(英_按位左移); *++stack = LT;  当前单词数据类型 = 数据类型_整数; }
        else if (当前单词编码 == 英_大于号) { 取下个单词(); *++stack = 变量入栈; 解析表达式(英_按位左移); *++stack = GT;  当前单词数据类型 = 数据类型_整数; }
        else if (当前单词编码 == 英_小于等于) { 取下个单词(); *++stack = 变量入栈; 解析表达式(英_按位左移); *++stack = LE;  当前单词数据类型 = 数据类型_整数; }
        else if (当前单词编码 == 英_大于等于) { 取下个单词(); *++stack = 变量入栈; 解析表达式(英_按位左移); *++stack = GE;  当前单词数据类型 = 数据类型_整数; }
        else if (当前单词编码 == 英_按位左移) { 取下个单词(); *++stack = 变量入栈; 解析表达式(英_加号); *++stack = SHL; 当前单词数据类型 = 数据类型_整数; }
        else if (当前单词编码 == 英_按位右移) { 取下个单词(); *++stack = 变量入栈; 解析表达式(英_加号); *++stack = SHR; 当前单词数据类型 = 数据类型_整数; }
        else if (当前单词编码 == 英_加号)
        {
            取下个单词(); *++stack = 变量入栈; 解析表达式(英_星号);
            if ((当前单词数据类型 = 参数计数) > 数据类型_指针) { *++stack = 变量入栈; *++stack = 加载到全局变量; *++stack = sizeof(int); *++stack = MUL; }
            *++stack = ADD;
        }
        else if (当前单词编码 == 英_减号)
        {
            取下个单词(); *++stack = 变量入栈; 解析表达式(英_星号);
            if (参数计数 > 数据类型_指针 && 参数计数 == 当前单词数据类型) { *++stack = SUB; *++stack = 变量入栈; *++stack = 加载到全局变量; *++stack = sizeof(int); *++stack = DIV; 当前单词数据类型 = 数据类型_整数; }
            else if ((当前单词数据类型 = 参数计数) > 数据类型_指针) { *++stack = 变量入栈; *++stack = 加载到全局变量; *++stack = sizeof(int); *++stack = MUL; *++stack = SUB; }
            else *++stack = SUB;
        }
        else if (当前单词编码 == 英_星号) { 取下个单词(); *++stack = 变量入栈; 解析表达式(英_双加号); *++stack = MUL; 当前单词数据类型 = 数据类型_整数; }
        else if (当前单词编码 == 英_除号) { 取下个单词(); *++stack = 变量入栈; 解析表达式(英_双加号); *++stack = DIV; 当前单词数据类型 = 数据类型_整数; }
        else if (当前单词编码 == 英_百分号) { 取下个单词(); *++stack = 变量入栈; 解析表达式(英_双加号); *++stack = MOD; 当前单词数据类型 = 数据类型_整数; }
        else if (当前单词编码 == 英_双加号 || 当前单词编码 == 英_双减号)
        {//处理x++,x--
            if (*stack == 加载字符) { *stack = 变量入栈; *++stack = 加载字符; }
            else if (*stack == 加载整数) { *stack = 变量入栈; *++stack = 加载整数; }
            else { printf("%d: 增量后左值错误\n", 当前行号); exit(-1); }
            *++stack = 变量入栈; *++stack = 加载到全局变量; *++stack = (当前单词数据类型 > 数据类型_指针) ? sizeof(int) : sizeof(char);
            *++stack = (当前单词编码 == 英_双加号) ? ADD : SUB;//先自增/自减
            *++stack = (当前单词数据类型 == 数据类型_字符) ? 保存字符 : 保存整数;//存到内存里
            *++stack = 变量入栈; *++stack = 加载到全局变量; *++stack = (当前单词数据类型 > 数据类型_指针) ? sizeof(int) : sizeof(char);
            *++stack = (当前单词编码 == 英_双加号) ? SUB : ADD;//再相反操作,保证后自增/自减不影响这次表达式的求值
            //PS:我终于知道哪些a=1;b=a+++a++为啥等于3了
            取下个单词();
        }
        else if (当前单词编码 == 英_左方括号)//数组下标
        {
            取下个单词(); *++stack = 变量入栈; 解析表达式(英_赋值号);//保存数组指针, 计算下标
            if (当前单词编码 == ']') 取下个单词(); else { printf("%d: 应为闭合右方括号\n", 当前行号); exit(-1); }
            if (参数计数 > 数据类型_指针) { *++stack = 变量入栈; *++stack = 加载到全局变量; *++stack = sizeof(int); *++stack = MUL; }//t==PTR时是Char,关键字_字符 = 0
            else if (参数计数 < 数据类型_指针) { printf("%d: 应为指针类型\n", 当前行号); exit(-1); }
            *++stack = ADD;
            *++stack = ((当前单词数据类型 = 参数计数 - 数据类型_指针) == 数据类型_字符) ? 加载字符 : 加载整数;
        }
        else { printf("%d: 编译错误 当前单词编码=%d\n", 当前行号, 当前单词编码); exit(-1); }
    }
}

//2.解析语句()负责分支循环语句的语法分析，包括if,while,return.碰到if，while,return，生成对应的零跳转,非零跳转,跳转的中间代码
void 解析语句()
{
    int* 寄存器变量, //*a:代表条件为false时跳转到的语句
    * b;  //*b:为了防止顺序执行if-else代码,所以执行了true部分时应跳过false部分
    // 1、解析if语句
    if (当前单词编码 == 关键字_如果)
    {
        取下个单词();
        if (当前单词编码 == '(') 取下个单词(); else { printf("%d: 应为左圆括号\n", 当前行号); exit(-1); }
        解析表达式(英_赋值号);
        // 如果当前tokens标记不是`)`，则打印错误并退出程序。
        if (当前单词编码 == ')') 取下个单词(); else { printf("%d: 应为右圆括号\n", 当前行号); exit(-1); }
        // 添加 jump-if-zero 指令 `零跳转` 以跳过真正的分支。将 `b` 指向稍后要修补的跳转地址字段。
        *++stack = 零跳转; b = ++stack;
        // 解析真分支的语句。
        解析语句();
        if (当前单词编码 == 关键字_否则) {
            // 修补 `b` 指向的跳转地址字段以保存,否则分支。`stack + 3` 不包括下面添加的 `跳转` 指令。在 true 分支后添加 `跳转` 指令以跳过 else分支。将 `b` 指向稍后要修补的跳转地址字段。
            *b = (int)(stack + 3); // e + 3 位置是else 起始位置
            *++stack = 跳转; // if 语句 else 之前插入 跳转 跳过Else 部分
            b = ++stack;    // 跳转目标
            取下个单词();
            解析语句();// 解析 else 分支的语句。
        }
        // 修补`b` 指向的跳转地址字段以保存过去的地址if-else 结构。
        *b = (int)(stack + 1);//if 语句结束,无论是if 零跳转 跳转目标还是 else 之前的跳转的跳转目标
    }
        // 2、解析while语句
    else if (当前单词编码 == 关键字_判断) {
        取下个单词();
        寄存器变量 = stack + 1;// 关键字_判断 循环体起始地址。将 `a` 指向循环的测试表达式的地址。
        // 如果当前tokens标记不是`(`，则打印错误并退出程序。
        if (当前单词编码 == '(') 取下个单词(); else { printf("%d: 应为左圆括号\n", 当前行号); exit(-1); }
        // 解析测试表达式。
        解析表达式(英_赋值号);
        // 如果当前tokens标记不是`)`，则打印错误并退出程序。
        if (当前单词编码 == ')') 取下个单词(); else { printf("%d: 应为右圆括号\n", 当前行号); exit(-1); }
        // 添加 jump-if-zero 指令 `零跳转` 以跳过循环体。将 `b` 指向稍后要修补的跳转地址字段。
        *++stack = 零跳转; b = ++stack;//b = 关键字_判断 语句结束后地址
        解析语句();//处理While 语句体
        // 添加`跳转`指令跳转到测试表达式。
        *++stack = 跳转; *++stack = (int)寄存器变量;//无条件跳转到While语句开始(包括循环条件的代码),实现循环
        // 修补`b` 指向的跳转地址字段以保存过去的地址循环结构。
        *b = (int)(stack + 1);//零跳转跳转目标(循环结束)
    }
        // 3、解析return语句
    else if (当前单词编码 == 关键字_返回) {
        // 读取令牌。
        取下个单词();
        // 如果当前tokens标记不是`;`，则为返回表达式。解析返回表达式。
        if (当前单词编码 != ';') 解析表达式(英_赋值号);//计算返回值
        // 添加`子程序退出`指令离开函数。
        *++stack = 子程序退出;// 销毁函数调用栈
        // 如果当前tokens标记为`;`，则读取tokens标记，否则打印错误并退出程序。
        if (当前单词编码 == ';') 取下个单词(); else { printf("%d: 应为分号\n", 当前行号); exit(-1); }
    }
        // 4、解析复合语句。如果当前令牌是`{`，则是块。
    else if (当前单词编码 == '{') {
        取下个单词();
        // 当前令牌不是`}`。解析语句。
        while (当前单词编码 != '}') 解析语句();
        // 读取令牌。
        取下个单词();
    }
        // 5、解析单句
    else if (当前单词编码 == ';') {
        // 读取令牌。
        取下个单词();
    }
        // 如果当前tokens标记不是上述任何一个，则假定它是表达式。
    else {
        // 解析表达式。
        解析表达式(英_赋值号);//一般的语句认为是赋值语句/表达式
        // 如果当前tokens标记为`;`，则读取tokens标记，否则打印错误并退出程序。
        if (当前单词编码 == ';') 取下个单词(); else { printf("%d: 应为分号\n", 当前行号); exit(-1); }
    }
}


//打开文件、分配内存、初始化函数、语法声明分析、虚拟机解释等功能
int 开始(int argc, char** argv)
{
    //C4的编译流程：词法分析->语法分析->语义分析（把声明记录到符号表，记录声明类型，记录表达式类型等）->代码生成->【目标虚拟机代码】

//	1.建立系统符号表
//	2.读取源代码
//	3.一次遍历源代码，同时进行词法分析，语法分析和中间代码生成。
//	4.执行中间代码

    //变量和函数的声明部分使用“递归下降”方式，在表达式部分采用递归下降与“运算符优先级”方式混合的做法。1.对单目运算符、函数调用、单个变量名等的部分可以说是用了递归下降方式。2.对双目运算符采用了运算符优先级方式

// 中间代码生成：
//	c4是在语法分析的过程中生成中间代码的。
//	main中，碰到枚举体定义时，更新符号表。碰到全局变量定义时，加入data area和符号表。碰到函数定义时，生成开辟新子程序和子程序退出的中间代码
//	stmt中，碰到if，while,return，生成对应的零跳转,非零跳转,跳转的中间代码
//	expr中，根据不同的预算符，将变量计算，并将结果均写入到寄存器中。

// 执行中间代码：
//	在符号表中确定main函数的位置。
//	初始化退出代码，变量入栈和EXIT
//	根据text area，执行中间代码，不断更新stack area与data area

    int i,
            源文件描述,
            基本类型,
            当前单词数据类型,
            缓冲区大小,
            *main函数的符号,
            *临时缓冲,
            执行指令计数;
    int *下条要执行的指令,
        *栈顶指针,
        *基址指针,
        寄存器变量;

    --argc; ++argv;// 递减 `argc` 以获取命令行参数的数量。增加 `argv` 以指向第一个命令行参数。
    if (argc > 0 && **argv == '-' && (*argv)[1] == 's') { 输出源码和汇编 = 1; --argc; ++argv; }// zhi -s hello.c， 打开打印源代码行和对应的开关。
    if (argc > 0 && **argv == '-' && (*argv)[1] == 'd') { 打印执行的指令 = 1; --argc; ++argv; }// zhi -d hello.c，打开调试开关。

    if (argc > 0 && **argv == '-' && (*argv)[1] == 'h') //zhi -h
    {
        printf("编译器帮助命令选项:\n");
        printf("-help  Help选项\n");
        printf("-s  打开打印源代码行和相应的开关(c4 -s hello.c)\n");
        printf("-d  输出汇编语言(c4 -d hello.c) \n"); return -1;
    }

    if (argc < 1) // zhi
    {
        printf("usage: c4 [-s] [-d] 源文件 ...\n");
        return -1;
    }
    if ((源文件描述 = open(*argv, 0)) < 0) //打开源文件,并读取到"源文件缓冲"中
    {
        printf("无法打开(%s)\n", *argv);
        return -1;
    }
    缓冲区大小 = 256 * 1024; // 定义一个256K变量.设置缓冲区大小。
    //给各区域申请空间
    if (!(符号表 = malloc(缓冲区大小))) { printf("无法对(%d) 符号表区域进行分配\n", 缓冲区大小); return -1; }// 分配符号表，如果失败，打印错误并退出程序。
    if (!(代码的段 = stack = malloc(缓冲区大小))) { printf("无法对(%d) 代码区域进行分配\n", 缓冲区大小); return -1; }// 分配指令缓冲区， 如果失败，打印错误并退出程序。
    if (!(数据的段 = malloc(缓冲区大小))) { printf("无法对(%d) 数据指针或bss指针区域进行分配\n", 缓冲区大小); return -1; }// 分配数据缓冲区，如果失败，打印错误并退出程序。
    if (!(栈顶指针 = malloc(缓冲区大小))) { printf("无法对(%d) 当前栈顶指针区域进行进行分配\n", 缓冲区大小); return -1; }// 分配堆栈， 如果失败，打印错误并退出程序。

    // 起始位置
    memset(符号表, 0, 缓冲区大小);
    memset(stack, 0, 缓冲区大小);
    memset(数据的段, 0, 缓冲区大小);

    // 用词法分析器先把这些关键词放进符号表
    源文件缓冲 = "char else enum if int return sizeof while "
               "open read close printf malloc free memset memcmp exit void main";
    i = 关键字_字符;
    while (i <= 关键字_判断) // 预先把关键词加进去,id[Tk]修改为和Enum一致。从 `char` 到 `while` 的每个关键字， 调用`取下个单词`来创建符号表条目，将关键字的单词类型存储在符号表条目的 `单词的编码` 字段中。
    {
        取下个单词();
        当前标识符[单词的编码] = i++;
    }
    //预先把[include]系统库里定义的符号(系统函数等) 加进去 单词的存储类别 赋值为Sys
    //对于从 `open` 到 `exit` 的每个系统函数名称，调用`取下个单词`来创建符号表条目，将符号表条目的符号类型字段设置为 `单词类别_系统调用`，将符号表条目的关联值类型字段设置为系统调用的返回类型，将符号表条目的关联值字段设置为系统调用的操作码。
    i = OPEN;
    while (i <= EXIT)
    {
        取下个单词();
        当前标识符[单词的存储类别] = 单词类别_系统调用;
        当前标识符[单词值的数据类型] = 数据类型_整数;
        当前标识符[单词的值] = i++;
    }
    取下个单词();
    当前标识符[单词的编码] = 关键字_字符; // 为 `void` 创建符号表条目。void 认为是char，不支持void类型，将void转为char
    取下个单词();
    main函数的符号 = 当前标识符; // 跟踪main信息，为 `main` 创建符号表条目。将 `main函数的符号` 指向符号表条目。记录main函数的符号id

    if (!(在源码中的当前位置 = 源文件缓冲 = malloc(缓冲区大小)))
    {
        printf("无法对(%d) 源代码区域进行分配\n", 缓冲区大小);
        return -1;
    }
    if ((i = read(源文件描述, 源文件缓冲, 缓冲区大小 - 1)) <= 0)
    {
        printf("read() 返回 %d\n", i);
        return -1;
    }// 从源文件中读取源代码到源代码缓冲区。
    源文件缓冲[i] = 0;// 在源代码缓冲区中的源代码后添加结束tokens标记`\0`。
    close(源文件描述);// 关闭源文件。

    // 语法分析。主流程的语法分析，包括全局变量定义，枚举体定义，函数定义
    当前行号 = 1;
    取下个单词();
    while (当前单词编码)
    {

        基本类型 = 数据类型_整数;
        //1.解析数据类型
        if (当前单词编码 == 关键字_整数)
        {
            取下个单词();// 上面已经设置“基本类型”=数据类型_整数。
        }
        else if (当前单词编码 == 关键字_字符)
        {
            取下个单词();
            基本类型 = 数据类型_字符;
        }
            // 枚举定义。更新符号表
        else if (当前单词编码 == 关键字_枚举)
        {
            取下个单词();
            if (当前单词编码 != '{')
            {
                取下个单词();//跳过枚举类型名称。仅支持匿名枚举。
            }
            if (当前单词编码 == '{')
            {
                取下个单词();
                i = 0;// 枚举值从 0 开始。
                while (当前单词编码 != '}')
                {
                    // 当前tokens标记应该是枚举名称。如果当前令牌不是标识符，则打印错误并退出程序。
                    if (当前单词编码 != 单词类别_标识符) //枚举成员赋值有两种方式,情况1：枚举成员的值默认从0开始
                    {
                        printf("%d: 枚举成员标识符 %d 错误\n", 当前行号, 当前单词编码);
                        return -1;
                    }
                    取下个单词();
                    if (当前单词编码 == 英_赋值号) //情况2：对枚举成员自定义赋值。 发现赋值语句 如 enum { 单词类别_常量 = 128 };
                    {
                        取下个单词();

                        if (当前单词编码 != 单词类别_常量) // 如果当前tokens标记不是数字常量，则打印错误并退出
                        {
                            printf("%d: 错误的枚举初始值设定项\n", 当前行号); return -1;
                        }
                        i = 当前单词值;// 设置枚举值。
                        取下个单词();
                    }
                    //将当前的标识加入符号表中。 `当前标识符` 指向枚举名称的符号表条目。将符号表条目的符号类型设置为 `单词类别_常量`。将符号表条目的关联值类型设置为 `数据类型_整数`。将符号表条目的关联值设置为枚举值。
                    当前标识符[单词的存储类别] = 单词类别_常量;
                    当前标识符[单词值的数据类型] = 数据类型_整数;
                    当前标识符[单词的值] = i++;
                    if (当前单词编码 == ',')//跳过‘,’
                    {
                        取下个单词();
                    }
                }
                取下个单词();// 跳过 `}`.
            }
        }
        //2.解析函数声明或变量定义
        while (当前单词编码 != ';' && 当前单词编码 != '}') // 当前tokens标记不是语句结束或块结束。
        {
            // 设置值类型。
            当前单词数据类型 = 基本类型;
            while (当前单词编码 == 英_星号)
            {
                取下个单词();
                当前单词数据类型 = 当前单词数据类型 + 数据类型_指针;// 当前单词数据类型 + 数据类型_指针：表示整数（字符）类型的指针
            }
            if (当前单词编码 != 单词类别_标识符) { printf("%d: 错误的全局标识符声明\n", 当前行号); return -1; }
            if (当前标识符[单词的存储类别]) { printf("%d: 重复的全局标识符定义\n", 当前行号); return -1; }//重复全局变量定义,解释见后
            取下个单词();
            当前标识符[单词值的数据类型] = 当前单词数据类型;//存储变量的数据类型或函数的返回类型。赋值类型

            if (当前单词编码 == '(')
            { // 如果当前tokens标记为`(`，则为函数定义。
                当前标识符[单词的存储类别] = 单词类别_函数;
                // 存储函数地址。`+1` 是因为添加指令的代码总是使用 `++stack`。
                当前标识符[单词的值] = (int)(stack + 1);//函数指针? 在字节码中的偏移量/地址
                取下个单词();
                i = 0;//i` 是参数的索引。
                while (当前单词编码 != ')') //解析参数列表
                {
                    当前单词数据类型 = 数据类型_整数;// 设置当前参数的数据类型。
                    if (当前单词编码 == 关键字_整数) 取下个单词();// 如果当前参数的数据类型为 `int`，则读取tokens标记。
                    else if (当前单词编码 == 关键字_字符) { 取下个单词(); 当前单词数据类型 = 数据类型_字符; }// 如果当前参数的数据类型是`char`，读取token，设置数据类型为`数据类型_字符`。
                    while (当前单词编码 == 英_星号) { 取下个单词(); 当前单词数据类型 = 当前单词数据类型 + 数据类型_指针; }// 当前tokens标记为 `*`，但它是指针类型。将 `数据类型_指针` 添加到数据类型。

                    if (当前单词编码 != 单词类别_标识符) { printf("%d: 错误的参数声明\n", 当前行号); return -1; }// 当前tokens标记应该是参数名称。如果当前令牌不是标识符，则打印错误并退出程序。
                    if (当前标识符[单词的存储类别] == 单词类别_局部变量) { printf("%d: 重复参数定义\n", 当前行号); return -1; }//函数参数是局部变量。 如果之前已经定义了参数名作为参数，则打印出错并退出程序。
                    当前标识符[备份_单词的类别] = 当前标识符[单词的存储类别]; 当前标识符[单词的存储类别] = 单词类别_局部变量;// 备份单词的`单词的存储类别`、`单词值的数据类型`、`单词的值` 字段，因为它们将临时用于参数名称。设置符号类型为局部变量。将关联的值类型设置为参数的数据类型。存储参数的索引。
                    当前标识符[备份_单词的数据类型] = 当前标识符[单词值的数据类型];  当前标识符[单词值的数据类型] = 当前单词数据类型;
                    当前标识符[备份_变量的值] = 当前标识符[单词的值];   当前标识符[单词的值] = i++;
                    取下个单词();
                    if (当前单词编码 == ',') 取下个单词();// 如果当前tokens标记为`,`，则跳过。
                }
                取下个单词();
                if (当前单词编码 != '{') { printf("%d: 错误的函数定义\n", 当前行号); return -1; }
                局部变量偏移 = ++i;
                取下个单词();
                while (当前单词编码 == 关键字_整数 || 当前单词编码 == 关键字_字符) //当前tokens标记是 `int` 或 `char`，它是变量定义。函数内变量声明
                {
                    基本类型 = (当前单词编码 == 关键字_整数) ? 数据类型_整数 : 数据类型_字符;// 设置基本数据类型。
                    取下个单词();
                    while (当前单词编码 != ';')
                    {
                        当前单词数据类型 = 基本类型;
                        while (当前单词编码 == 英_星号) { 取下个单词(); 当前单词数据类型 = 当前单词数据类型 + 数据类型_指针; }//处理指针型。
                        if (当前单词编码 != 单词类别_标识符) { printf("%d: 错误的局部变量声明\n", 当前行号); return -1; }// 当前tokens标记应该是局部变量名。如果当前令牌不是标识符，则打印错误并退出程序
                        if (当前标识符[单词的存储类别] == 单词类别_局部变量) { printf("%d: 重复的局部定义\n", 当前行号); return -1; }// 如果局部变量名之前已经定义为local变量，打印错误并退出程序。
                        当前标识符[备份_单词的类别] = 当前标识符[单词的存储类别]; 当前标识符[单词的存储类别] = 单词类别_局部变量;//备份符号信息,要进入函数上下文了。备份参数的`单词的存储类别`、`单词值的数据类型`、`单词的值` 字段，因为它们将临时用于局部变量名称。设置符号类型为局部变量。将关联的值类型设置为局部变量的数据类型。存储局部变量的索引。
                        当前标识符[备份_单词的数据类型] = 当前标识符[单词值的数据类型];  当前标识符[单词值的数据类型] = 当前单词数据类型;
                        当前标识符[备份_变量的值] = 当前标识符[单词的值];   当前标识符[单词的值] = ++i;//局部变量编号//存储变量偏移量
                        取下个单词();
                        if (当前单词编码 == ',') 取下个单词();
                    }
                    取下个单词();
                }
                *++stack = 开辟新子程序; *++stack = i - 局部变量偏移;//为局部变量的入栈申请空间。函数局部变量数目。在函数体之前添加 `开辟新子程序` 指令。添加局部变量 参数计数 作为操作数。
                while (当前单词编码 != '}') 解析语句();// 虽然当前tokens标记不是函数体的结尾`}`，解析语句。
                *++stack = 子程序退出;//函数返回,解析完毕,弹栈。在函数体之后添加 `子程序退出` 指令。
                当前标识符 = 符号表; //恢复全局变量的信息。 将 `当前标识符` 指向符号表。
                while (当前标识符[单词的编码])
                {
                    if (当前标识符[单词的存储类别] == 单词类别_局部变量)
                    {// 如果符号表条目用于函数参数或局部变量。恢复符号信息
                        当前标识符[单词的存储类别] = 当前标识符[备份_单词的类别];// 恢复 `单词的存储类别`、`单词值的数据类型` 和 `单词的值` 字段的旧值。
                        当前标识符[单词值的数据类型] = 当前标识符[备份_单词的数据类型];
                        当前标识符[单词的值] = 当前标识符[备份_变量的值];
                    }
                    当前标识符 = 当前标识符 + 符号表元素个数;// 指向下一个符号表条目。
                }
            }
            else {// 如果当前tokens标记不是`(`，则不是函数定义，假设它是全局变量定义。加入data area和符号表
                当前标识符[单词的存储类别] = 单词类别_全局变量;//设置符号类型：全局变量
                当前标识符[单词的值] = (int)数据的段;//给全局变量在data段分配内存地址
                数据的段 = 数据的段 + sizeof(int);// 指向下一个全局变量。
            }
            if (当前单词编码 == ',') 取下个单词();
        }
        取下个单词();
    }


    // 将指令指针 `下条要执行的指令` 指向 `main` 函数的地址。如果符号`main`的`单词的值`字段未设置，则表示`main`函数是未定义，打印错误并退出程序。
    if (!(下条要执行的指令 = (int*)main函数的符号[单词的值])) { printf("开始()函数未定义\n"); return -1; }
    // 如果打印源代码行和对应指令的开关是打开，退出程序。
    if (输出源码和汇编) return 0;
    基址指针 = 栈顶指针 = (int*)((int)栈顶指针 + 缓冲区大小);// 设置栈,将帧基指针`基址指针`和栈顶指针`栈顶指针`指向栈底。
    *--栈顶指针 = EXIT; // 如果main函数返回就调用exit.// 将 `EXIT` 指令压入堆栈。注意堆栈向低地址增长，所以在`变量入栈`指令之后下面添加的是执行，这条`EXIT`指令会被执行退出该程序。
    *--栈顶指针 = 变量入栈; 临时缓冲 = 栈顶指针;// 将 `变量入栈` 指令压入堆栈将寄存器中的退出代码压入堆栈在 `main` 函数返回之后。 堆栈上的退出代码将由上面添加了 `EXIT` 指令。将 `t` 指向 `变量入栈` 指令的地址。
    *--栈顶指针 = argc;// 将 `main` 函数的第一个参数 `argc` 压入堆栈。
    *--栈顶指针 = (int)argv;// 将 `main` 函数的第二个参数 `argv` 压入堆栈。
    *--栈顶指针 = (int)临时缓冲;// 将 `变量入栈` 指令的地址压入堆栈，以便 `main` 函数将返回到 `变量入栈` 指令。
    执行指令计数 = 0;// 跑步...执行指令计数。
    // 虚拟机运行，循环来执行虚拟机指令。
    while (1)
    {
        // 获取当前指令。递增指令指针。增加指令周期数。
        i = *下条要执行的指令++; ++执行指令计数;
        // 如果调试开关打开。
        if (打印执行的指令)
        {
            // 打印汇编指令操作码。
            printf("%d> %.4s", 执行指令计数,
                   &"加载到局部变量 ,加载到全局变量 ,跳转 ,跳转到子程序 ,零跳转  ,非零跳转 ,开辟新子程序 ,栈跳转 ,子程序退出 ,加载整数  ,加载字符  ,保存整数  ,保存字符  ,变量入栈 ,"
                    "OR  ,XOR ,AND ,EQ  ,NE  ,LT  ,GT  ,LE  ,GE  ,SHL ,SHR ,ADD ,SUB ,MUL ,DIV ,MOD ,"
                    "OPEN,READ,CLOS,PRTF,MALC,FREE,MSET,MCMP,EXIT,"[i * 5]);
            // 如果操作码 <= 栈跳转，它有操作数。 打印操作数。
            if (i <= 栈跳转) printf(" %d\n", *下条要执行的指令); else printf("\n");
        }
        // 将帧基指针 `基址指针` 中的基地址添加到操作数。
        if (i == 加载到局部变量)
        {
            寄存器变量 = (int)(基址指针 + *下条要执行的指令++);        //本地地址，加载要注册的操作数。
        }
        else if (i == 加载到全局变量)
        {
            寄存器变量 = *下条要执行的指令++;// 全局地址或立即数
        }
        else if (i == 跳转) 下条要执行的指令 = (int*)*下条要执行的指令;                                   // 将第二个操作数中的返回地址压入堆栈。 跳转到第一个操作数中的地址。
        else if (i == 跳转到子程序) { *--栈顶指针 = (int)(下条要执行的指令 + 1); 下条要执行的指令 = (int*)*下条要执行的指令; }        // 将第二个操作数中的返回地址压入堆栈。跳转到第一个操作数中的地址。
        else if (i == 零跳转)  下条要执行的指令 = 寄存器变量 ? 下条要执行的指令 + 1 : (int*)*下条要执行的指令;                      //子程序 如果寄存器值为 0，则跳转到第一个操作数中的地址。
        else if (i == 非零跳转) 下条要执行的指令 = 寄存器变量 ? (int*)*下条要执行的指令 : 下条要执行的指令 + 1;                      // 如果寄存器值不为 0，则跳转到第一个操作数中的地址。
        else if (i == 开辟新子程序) { *--栈顶指针 = (int)基址指针; 基址指针 = 栈顶指针; 栈顶指针 = 栈顶指针 - *下条要执行的指令++; }     // 如果不为零则分支    将`基址指针`中调用者的帧基地址压入堆栈。 将`基址指针`指向被调用者的栈顶。 将栈顶指针 `栈顶指针` 减少操作数中的值以保留      // 被调用者的局部变量的空间。
        else if (i == 栈跳转) 栈顶指针 = 栈顶指针 + *下条要执行的指令++;                                  // 进入子程序  从函数调用返回后从堆栈中弹出参数。
        else if (i == 子程序退出) { 栈顶指针 = 基址指针; 基址指针 = (int*)*栈顶指针++; 下条要执行的指令 = (int*)*栈顶指针++; } // 在调用之前将栈顶指针 `栈顶指针` 指向调用者的栈顶。将调用者的帧基地址从堆栈弹出到 `基址指针`。旧值被`开辟新子程序`指令压入堆栈。
        else if (i == 加载整数) 寄存器变量 = *(int*)寄存器变量;// 离开子程序，在寄存器中的地址上加载 int 值到寄存器。
        else if (i == 加载字符) 寄存器变量 = *(char*)寄存器变量;// 加载整数，在寄存器中的地址上加载字符值以进行注册。
        else if (i == 保存整数)  *(int*)*栈顶指针++ =寄存器变量;// 加载字符，将寄存器中的 int 值保存到堆栈中的地址。
        else if (i == 保存字符) 寄存器变量 = *(char*)*栈顶指针++ =寄存器变量;// 存储整数，将寄存器中的字符值保存到堆栈中的地址。
        else if (i == 变量入栈) *--栈顶指针 =寄存器变量;// 存储字符，将寄存器值压入堆栈。
            // 以下指令采用两个参数。第一个参数在堆栈上。 第二个参数在寄存器中。 结果存入寄存器。
        else if (i == OR) 寄存器变量 = *栈顶指针++ |寄存器变量;
        else if (i == XOR)寄存器变量 = *栈顶指针++ ^寄存器变量;
        else if (i == AND)寄存器变量 = *栈顶指针++ &寄存器变量;
        else if (i == EQ) 寄存器变量 = *栈顶指针++ ==寄存器变量;
        else if (i == NE) 寄存器变量 = *栈顶指针++ !=寄存器变量;
        else if (i == LT) 寄存器变量 = *栈顶指针++ <寄存器变量;
        else if (i == GT) 寄存器变量 = *栈顶指针++ >寄存器变量;
        else if (i == LE) 寄存器变量 = *栈顶指针++ <=寄存器变量;
        else if (i == GE) 寄存器变量 = *栈顶指针++ >=寄存器变量;
        else if (i == SHL)寄存器变量 = *栈顶指针++ <<寄存器变量;
        else if (i == SHR)寄存器变量 = *栈顶指针++ >>寄存器变量;
        else if (i == ADD)寄存器变量 = *栈顶指针++ +寄存器变量;
        else if (i == SUB)寄存器变量 = *栈顶指针++ -寄存器变量;
        else if (i == MUL)寄存器变量 = *栈顶指针++ *寄存器变量;
        else if (i == DIV)寄存器变量 = *栈顶指针++ /寄存器变量;
        else if (i == MOD)寄存器变量 = *栈顶指针++ %寄存器变量;

//内置函数功能实现：systemWord中定义的内置函数功能实现，下面的指令是系统调用。它们从堆栈中获取参数，就像用户定义的函数一样。注意堆栈向低地址增长，较早推送的参数是在更高的地址。

            // 判断是否为文件打开函数open()，例如。 如果堆栈上有三个参数，则：`栈顶指针[2]` 是第一个参数。`栈顶指针[1]` 是第二个参数。`*栈顶指针` 是第三个参数。打开文件。Arg 1: 要打开的文件路径。参数 2：标志。
        else if (i == OPEN)寄存器变量 = open((char*)栈顶指针[1], *栈顶指针);
            //判断是否为文件读取函数read()， 从文件描述符读取到缓冲区。参数 1: 文件描述符。参数 2: 缓冲区指针。参数 3: 要读取的字节数。
        else if (i == READ)寄存器变量 = read(栈顶指针[2], (char*)栈顶指针[1], *栈顶指针);
            // 判断是否为文件读取函数close()，关闭文件描述符。参数 1: 文件描述符。
        else if (i == CLOS)寄存器变量 = close(*栈顶指针);
            // 打印格式化字符串。因为调用有参数，所以 栈跳转 指令应该是添加。 `下条要执行的指令[1]` 获取 栈跳转 指令的操作数，即参数。参数 1: 格式字符串。参数 2-7：格式化的值。
        else if (i == PRTF) { 临时缓冲 = 栈顶指针 + 下条要执行的指令[1];寄存器变量 = printf((char*)临时缓冲[-1], 临时缓冲[-2], 临时缓冲[-3], 临时缓冲[-4], 临时缓冲[-5], 临时缓冲[-6]); }
            //判断是否为内存分配函数malloc()，分配内存块。参数 1: 要分配的字节数。
        else if (i == MALC)寄存器变量 = (int)malloc(*栈顶指针);
            //判断是否为内存释放函数free()，分配的空闲内存块。参数 1: 内存块指针。
        else if (i == FREE) free((void*)*栈顶指针);
        else if (i == MSET)寄存器变量 = (int)memset((char*)栈顶指针[2], 栈顶指针[1], *栈顶指针);// 判断是否为内存释放函数memset()，将内存缓冲区中的每个字节设置为相同的值。参数 1: 缓冲区指针。参数 2：值。参数 3: 要设置的字节数。
        else if (i == MCMP)寄存器变量 = memcmp((char*)栈顶指针[2], (char*)栈顶指针[1], *栈顶指针);//判断是否为内存释放函数memcmp()，比较内存缓冲区。参数 1: 第一个缓冲区指针。参数 2: 第二个缓冲区指针。参数 3：要比较的字节数。
        else if (i == EXIT) { printf("exit(%d) 执行指令计数 = %d\n", *栈顶指针, 执行指令计数); return *栈顶指针; }//判断是否为内存释放函数exit()，退出程序。参数 1: 退出代码。
            //您可以在此处扩展其它自定义系统函数功能
            // ....
            //扩展功能结束
            // 当前指令未知，打印错误并退出程序。
        else { printf("未知指令 = %d! 执行指令计数 = %d\n", i, 执行指令计数); return -1; }
    }
}
